Webpack is a packaging tool used to create modular bundles for JavaScript, CSS and HTML. Though this is it’s main feature, there are many other features of webpack as well – such as being a task runner and even acting as a dev server. By the use of a dependency graph, it maps out the bundles for the project such that a client needs only download those bundles it requires at that time, thus elevates the need to download an entire application.
Modular bundling depends on the dependency graph. That can look like so:
The dependency graph shows all the components at should be included for this bundle and the entry point to the bundle (app.js).
This post are some examples I followed to help me understand the tool and features. Before looking at the code though, there are 5 core concepts of the WebPack tool which are covered below.
Entry
An entry point iindicates which module webpack should use to begin building out the internal dependency graph. By default this is ./src/index.js. Multiple entry points can be defined in the webpack configuration file (webpack.config.js).
module.exports = {
entry: './path/to/my/entry/file.js'
};
Output
The output property tells webpack where to emit the bundles and how to name the files. It defaults to ./dist/main.js.
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
}
};
Loaders
Loaders provide webpack the ability to process different file types and incorporate them into the bundled modules. At a high level – loaders have two properties in your webpack configuraiton:
- The test property identifies which files should be transformed
- The use property indicates which loader should be used to do the transforming
const path = require('path');
module.exports = {
output: {
filename: 'my-first-webpack.bundle.js'
},
module: {
rules: [
{
test: /\.txt$/,
use: 'raw-loader'
}
]
}
};
Plugins
Plugins are additional functionalities that can be applied to the bundles. This includes things like optimizations (minification, tree shaking, etc), asset management, and injection of environment variables. Plugins will need a ‘require’ statement at the top of the webpack configuration to bring in that specific plugin.
const HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm
const webpack = require('webpack'); //to access built-in plugins
module.exports = {
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' }
]
},
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
]
};
There are many plugins available out-of-the-box with webpack. They are listed here:
https://webpack.js.org/plugins/
Mode
The mode setting toggles which optimizations webpack will use during its build process. This is used to set the different environments – development, production or none.
module.exports = {
mode: 'production'
};
Stats
Webpack can generate stats to show how the modules are generated. It can be used to analyze an application’s dependency graph and to optimize compilation speed. The following command generates the stats file:
webpack --profile --json > stats.json
The file has various parts to it which describe the over all application’s build path. More details about how to read this file can be found here:
https://webpack.js.org/api/stats/
There is a useful plugin that can visualize the stats file. This visualization makes it very easy to see the inner dependencies of the modules. This can be done here:
https://github.com/webpack/analyse
Sample Project
This is a sample project created for my church using Material Design Components on jQuery. Its a very simple app but demonstrates how Webpack can be used to package up the files. The full source code can be viewed here:
https://github.com/revivepres/chdir
The package.json contains some of the basic dev-dependencies needed to create the webpack bundles. You can also see the Material Design Components being defined here as well as a ‘build’ script definition for running webpack building with the production flag (“-p”).
{ "name": "chdir", "private": true, "version": "0.0.0", "scripts": { "start": "webpack-dev-server --progress", "build": "webpack -p" }, "dependencies": { "@material/button": "^0.35.0", "@material/checkbox": "^0.39.3", "@material/layout-grid": "^0.39.0", "@material/list": "^0.35.0", "@material/rtl": "^0.39.1", "@material/snackbar": "^0.39.1", "@material/textfield": "^0.35.0", "@material/top-app-bar": "^0.39.3" }, "devDependencies": { "babel-core": "^6.22.1", "babel-loader": "^7.0.0", "babel-preset-env": "^1.7.0", "css-loader": "^0.28.0", "extract-loader": "^1.0.2", "file-loader": "^1.1.11", "node-sass": "^4.9.0", "sass-loader": "^6.0.4", "webpack": "^3.0.0", "webpack-dev-server": "^2.4.3" } }
For this simple app there are two bundle files created, one for the css and one for the js. They are named as:
- bundle.css
- bundle.js
You can see these declared in the webpack.config.js file below. Note that in this configuration, there are two dependency graphs being built – one for the scss and one for the js. Usually we would draw a single dependency graph that would handle both css and js files. But this example shows how we can have multiple entry points.
module.exports = [ { entry: './app.scss', output: { // This is necessary for webpack to compile // But we never use style-bundle.js filename: 'style-bundle.js', }, module: { rules: [{ test: /\.scss$/, use: [ { loader: 'file-loader', options: { name: 'bundle.css', }, }, { loader: 'extract-loader' }, { loader: 'css-loader' }, { loader: 'sass-loader', options: { includePaths: ['./node_modules'], } }, ] }] }, }, { entry: "./app.js", output: { filename: "bundle.js" }, module: { loaders: [{ test: /\.js$/, loader: 'babel-loader', query: { presets: ['env'] } }] }, } ];
Usually we would use some type of webpack plugin to setup the index.html (see the lemoncode examples below in the references). However for this simple app, I have webpack simply create the two bundle files defined above and I manually reference it in the index.html file. (Usually webpack is able to update the index.html file via a plugin to include whatever bundle files are generated). Also note that for this simple example, we dont have any polyfils, manifest files, etc. Again, I was going for the most simple example here.
<!DOCTYPE html> <html> <head> <metacharset="utf-8"> <metaname="viewport"content="width=device-width, initial-scale=1"> <linkrel="stylesheet"href="bundle.css"> </head> <body> <headerclass="mdc-top-app-bar mdc-top-app-bar--fixed"> ... <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <script src="bundle.js" async></script> </body> </html>
Also note that in the example above I am referencing the jQuery library via the CDN instead of installing it (via NPM) and adding it as part of my bundle.js. Typically we would want include third-party libraries as part of our webpack bundles to reduce the number of fetches that the client needs to perform.
References
Webpack
https://webpack.js.org/guides/getting-started/
How Webpack Works – YGLF 2018
https://www.youtube.com/watch?v=Gc9-7PBqOC8
Webpack Plugins
https://webpack.js.org/plugins/
Webpack Tutorial / Samples
https://github.com/Lemoncode/webpack-by-sample